package api

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/direktiv/direktiv/internal/cluster/cache"
	"github.com/direktiv/direktiv/internal/cluster/pubsub"
	"github.com/direktiv/direktiv/internal/core"
	"github.com/direktiv/direktiv/internal/datastore/datasql"
	"github.com/direktiv/direktiv/internal/engine"
	"github.com/direktiv/direktiv/internal/extensions"
	"github.com/direktiv/direktiv/internal/sched"
	"github.com/direktiv/direktiv/internal/version"
	"github.com/direktiv/direktiv/pkg/lifecycle"
	"github.com/go-chi/chi/v5"
	"gorm.io/gorm"
)

const (
	readHeaderTimeout = 5 * time.Second
)

type Version struct {
	UnixTime int64 `json:"unix_time"`
}

type InitializeArgs struct {
	Version *Version
	Config  *core.Config
	PubSub  pubsub.EventBus

	ServiceManager core.ServiceManager
	CacheManager   cache.Manager

	RegistryManager core.RegistryManager
	GatewayManager  core.GatewayManager
	SecretsManager  core.SecretsManager

	Engine    *engine.Engine
	Scheduler *sched.Scheduler
	DB        *gorm.DB
}

type Server struct {
	srv *http.Server
}

func (s *Server) Start(lc *lifecycle.Manager) error {
	lc.Go(func() error {
		err := s.srv.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			return fmt.Errorf("listen and serve, err: %w", err)
		}

		return nil
	})

	return nil
}

func (s *Server) Close(ctx context.Context) error {
	return s.srv.Shutdown(ctx)
}

func New(app InitializeArgs) (*Server, error) {
	funcCtr := &serviceController{
		manager: app.ServiceManager,
	}

	fsCtr := &fsController{
		db:    app.DB,
		bus:   app.PubSub,
		cache: app.CacheManager.FlowCache(),
	}
	regCtr := &registryController{
		manager: app.RegistryManager,
	}
	varCtr := &varController{
		db: app.DB,
	}
	secCtr := &secretsController{
		secretsManager: app.SecretsManager,
	}
	nsCtr := &nsController{
		db:              app.DB,
		bus:             app.PubSub,
		engine:          app.Engine,
		registryManager: app.RegistryManager,
		cache:           app.CacheManager,
		sManager:        app.SecretsManager,
	}
	mirrorsCtr := &mirrorsController{
		db:  app.DB,
		bus: app.PubSub,
	}
	instCtr := &instController{
		db:        app.DB,
		manager:   nil,
		engine:    app.Engine,
		scheduler: app.Scheduler,
	}
	notificationsCtr := &notificationsController{
		sManager: app.SecretsManager,
	}
	metricsCtr := &metricsController{
		db:     app.DB,
		engine: app.Engine,
	}
	eventsCtr := eventsController{
		store:         datasql.NewStore(app.DB),
		wakeInstance:  nil,
		startWorkflow: nil,
	}

	jxCtr := jxController{}

	mw := &appMiddlewares{
		db:    app.DB,
		cache: app.CacheManager,
	}

	r := chi.NewRouter()
	r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
		writeError(w, &Error{
			Code:    "request_method_not_allowed",
			Message: "request http method is not allowed for this path",
		})
	})
	r.NotFound(func(w http.ResponseWriter, r *http.Request) {
		writeError(w, &Error{
			Code:    "request_path_not_found",
			Message: "request http path is not found",
		})
	})

	for _, extraRoute := range GetExtraRoutes() {
		extraRoute(r)
	}

	// version endpoint
	r.Get("/api/v2/status", func(w http.ResponseWriter, r *http.Request) {
		data := struct {
			Version      string `json:"version"`
			IsEnterprise bool   `json:"isEnterprise"`
			RequiresAuth bool   `json:"requiresAuth"`
		}{
			IsEnterprise: extensions.IsEnterprise,
			RequiresAuth: os.Getenv("DIREKTIV_UI_SET_API_KEY") == "true",
		}
		if version.Version != "" && version.GitSha != "" {
			data.Version = version.Version + " " + version.GitSha
		} else {
			data.Version = "dev2"
		}

		writeJSON(w, data)
	})

	logCtr := &logController{
		logsBackend: app.Config.LogsBackend,
	}
	r.Handle("/ns/{namespace}/*", app.GatewayManager)

	r.Route("/api/v2", func(r chi.Router) {
		r.Route("/namespaces", func(r chi.Router) {
			if extensions.CheckOidcMiddleware != nil {
				r.Use(
					extensions.CheckOidcMiddleware,
					extensions.CheckAPITokenMiddleware,
					extensions.CheckAPIKeyMiddleware)
			}
			nsCtr.mountRouter(r)
		})

		r.Group(func(r chi.Router) {
			if extensions.CheckOidcMiddleware != nil {
				r.Use(
					extensions.CheckOidcMiddleware,
					extensions.CheckAPITokenMiddleware,
					extensions.CheckAPIKeyMiddleware)
			}
			r.Use(mw.checkNamespace)

			r.Route("/namespaces/{namespace}/instances", func(r chi.Router) {
				instCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/syncs", func(r chi.Router) {
				mirrorsCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/secrets", func(r chi.Router) {
				secCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/variables", func(r chi.Router) {
				varCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/files", func(r chi.Router) {
				fsCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/services", func(r chi.Router) {
				funcCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/registries", func(r chi.Router) {
				regCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/logs", func(r chi.Router) {
				logCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/notifications", func(r chi.Router) {
				notificationsCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/metrics", func(r chi.Router) {
				metricsCtr.mountRouter(r)
			})
			r.Route("/namespaces/{namespace}/events/history", func(r chi.Router) {
				eventsCtr.mountEventHistoryRouter(r)
			})
			r.Route("/namespaces/{namespace}/events/listeners", func(r chi.Router) {
				eventsCtr.mountEventListenerRouter(r)
			})
			r.Route("/namespaces/{namespace}/events/broadcast", func(r chi.Router) {
				eventsCtr.mountBroadcast(r)
			})
			r.Handle("/namespaces/{namespace}/gateway/*", app.GatewayManager)

			if len(extensions.AdditionalAPIRoutes) > 0 {
				for pattern, ctr := range extensions.AdditionalAPIRoutes {
					r.Route(pattern, func(r chi.Router) {
						ctr(r)
					})
				}
			}
		})

		r.Route("/jx", func(r chi.Router) {
			jxCtr.mountRouter(r)
		})
	})

	r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
		if strings.Contains(r.URL.Path, "/api/") {
			writeError(w, &Error{
				Code:    "request_path_not_found",
				Message: "request http path is not found",
			})

			return
		}

		staticDir := "/app/ui"
		fs := http.FileServer(http.Dir(staticDir))

		if _, err := os.Stat(staticDir + r.URL.Path); err == nil {
			fs.ServeHTTP(w, r)
			return
		}

		http.ServeFile(w, r, filepath.Join(staticDir, "index.html"))
	})

	apiServer := &http.Server{
		Addr:              "0.0.0.0:" + strconv.Itoa(app.Config.ApiPort),
		Handler:           r,
		ReadHeaderTimeout: readHeaderTimeout,
	}

	return &Server{
		srv: apiServer,
	}, nil
}

func writeJSON(w http.ResponseWriter, v any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	payLoad := struct {
		Data any `json:"data"`
	}{
		Data: v,
	}
	err := json.NewEncoder(w).Encode(payLoad)
	if err != nil {
		panic(fmt.Errorf("write json: %w", err))
	}
}

func writeJSONWithMeta(w http.ResponseWriter, data any, meta any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)

	payload := struct {
		Meta any `json:"meta"`
		Data any `json:"data"`
	}{
		Data: data,
		Meta: meta,
	}
	_ = json.NewEncoder(w).Encode(payload)
}

func writeOk(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "application/json")

	w.WriteHeader(http.StatusOK)
}

// ParseQueryParam retrieves a query parameter as either string or int.
// T can be string or int.
func ParseQueryParam[T string | int](r *http.Request, name string, def T) T {
	s := r.URL.Query().Get(name)
	if s == "" {
		return def
	}

	switch any(def).(type) {
	case string:
		// If caller expects a string, just return it
		return any(s).(T)

	case int:
		// If caller expects an int, try parsing
		v, err := strconv.Atoi(s)
		if err != nil {
			return def
		}

		return any(v).(T)
	}

	// Should never reach here if used with correct type parameter
	return def
}
